﻿using Google.Protobuf.Reflection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace ProtoBuf.Reflection
{
    /// <summary>
    /// A coded generator that writes C#
    /// </summary>
    public class CSharpCodeGenerator : CommonCodeGenerator
    {
        /// <summary>
        /// Reusable code-generator instance
        /// </summary>
        public static CSharpCodeGenerator Default { get; } = new CSharpCodeGenerator();
        /// <summary>
        /// Create a new CSharpCodeGenerator instance
        /// </summary>
        protected CSharpCodeGenerator() { }
        /// <summary>
        /// Returns the language name
        /// </summary>
        public override string Name => "C#";
        /// <summary>
        /// Returns the default file extension
        /// </summary>
        protected override string DefaultFileExtension => "cs";
        /// <summary>
        /// Escapes language keywords
        /// </summary>
        protected override string Escape(string identifier)
        {
            switch (identifier)
            {
                case "abstract":
                case "event":
                case "new":
                case "struct":
                case "as":
                case "explicit":
                case "null":
                case "switch":
                case "base":
                case "extern":
                case "object":
                case "this":
                case "bool":
                case "false":
                case "operator":
                case "throw":
                case "break":
                case "finally":
                case "out":
                case "true":
                case "byte":
                case "fixed":
                case "override":
                case "try":
                case "case":
                case "float":
                case "params":
                case "typeof":
                case "catch":
                case "for":
                case "private":
                case "uint":
                case "char":
                case "foreach":
                case "protected":
                case "ulong":
                case "checked":
                case "goto":
                case "public":
                case "unchecked":
                case "class":
                case "if":
                case "readonly":
                case "unsafe":
                case "const":
                case "implicit":
                case "ref":
                case "ushort":
                case "continue":
                case "in":
                case "return":
                case "using":
                case "decimal":
                case "int":
                case "sbyte":
                case "virtual":
                case "default":
                case "interface":
                case "sealed":
                case "volatile":
                case "delegate":
                case "internal":
                case "short":
                case "void":
                case "do":
                case "is":
                case "sizeof":
                case "while":
                case "double":
                case "lock":
                case "stackalloc":
                case "else":
                case "long":
                case "static":
                case "enum":
                case "namespace":
                case "string":
                    return "@" + identifier;
                default:
                    return identifier;
            }
        }
        /// <summary>
        /// Start a file
        /// </summary>
        protected override void WriteFileHeader(GeneratorContext ctx, FileDescriptorProto file, ref object state)
        {
            ctx.WriteLine("// This file was generated by a tool; you should avoid making direct changes.")
               .WriteLine("// Consider using 'partial classes' to extend these types")
               .WriteLine($"// Input: {Path.GetFileName(ctx.File.Name)}").WriteLine()
               .WriteLine("#pragma warning disable CS1591, CS0612, CS3021").WriteLine();


            var @namespace = ctx.NameNormalizer.GetName(file);

            if (!string.IsNullOrWhiteSpace(@namespace))
            {
                state = @namespace;
                ctx.WriteLine($"namespace {@namespace}");
                ctx.WriteLine("{").Indent().WriteLine();
            }

        }
        /// <summary>
        /// End a file
        /// </summary>
        protected override void WriteFileFooter(GeneratorContext ctx, FileDescriptorProto file, ref object state)
        {
            var @namespace = (string)state;
            if (!string.IsNullOrWhiteSpace(@namespace))
            {
                ctx.Outdent().WriteLine("}").WriteLine();
            }
            var @filename = file.Name.Substring(0, file.Name.IndexOf("."));
            ctx.WriteLine($"public class ILRuntime_{@filename}");
            ctx.WriteLine("{").Indent();
            ctx.WriteLine($"static ILRuntime_{@filename}()");
            ctx.WriteLine("{");
            ctx.WriteLine().Indent();
            ctx.WriteLine($"//Initlize();");
            ctx.WriteLine().Outdent();
            ctx.WriteLine("}");
            ctx.WriteLine($"public static void Initlize()");
            ctx.WriteLine("{");
            ctx.WriteLine().Indent();
            foreach(var T in TypeNames2)
            {
                ctx.WriteLine($@"ProtoBuf.PType.RegisterType(""{T}"", typeof({T}));");
            }
            ctx.WriteLine().Outdent();
            ctx.WriteLine("}").Outdent();
            ctx.WriteLine("}").WriteLine();
            ctx.WriteLine("#pragma warning restore CS1591, CS0612, CS3021");
        }
        /// <summary>
        /// Start an enum
        /// </summary>
        protected override void WriteEnumHeader(GeneratorContext ctx, EnumDescriptorProto obj, ref object state)
        {
            var name = ctx.NameNormalizer.GetName(obj);
            var tw = ctx.Write($@"[global::ProtoBuf.ProtoContract(");
            if (name != obj.Name) tw.Write($@"Name = @""{obj.Name}""");
            tw.WriteLine(")]");
            WriteOptions(ctx, obj.Options);
            ctx.WriteLine($"{GetAccess(GetAccess(obj))} enum {Escape(name)}").WriteLine("{").Indent();
        }
        /// <summary>
        /// End an enum
        /// </summary>

        protected override void WriteEnumFooter(GeneratorContext ctx, EnumDescriptorProto obj, ref object state)
        {
            ctx.Outdent().WriteLine("}").WriteLine();
        }
        /// <summary>
        /// Write an enum value
        /// </summary>
        protected override void WriteEnumValue(GeneratorContext ctx, EnumValueDescriptorProto obj, ref object state)
        {
            var name = ctx.NameNormalizer.GetName(obj);
            if (name != obj.Name)
            {
                var tw = ctx.Write($@"[global::ProtoBuf.ProtoEnum(");
                tw.Write($@"Name = @""{obj.Name}""");
                tw.WriteLine(")]");
            }
            
            WriteOptions(ctx, obj.Options);
            ctx.WriteLine($"{Escape(name)} = {obj.Number},");
        }

        /// <summary>
        /// End a message
        /// </summary>
        protected override void WriteMessageFooter(GeneratorContext ctx, DescriptorProto obj, ref object state)
        {
            ctx.Outdent().WriteLine("}").WriteLine();
        }
        /// <summary>
        /// Start a message
        /// </summary>
        protected override void WriteMessageHeader(GeneratorContext ctx, DescriptorProto obj, ref object state)
        {
            var name = ctx.NameNormalizer.GetName(obj);
            GetTypeName2(obj.FullyQualifiedName);
            var tw = ctx.Write($@"[global::ProtoBuf.ProtoContract(");
            if (name != obj.Name) tw.Write($@"Name = @""{obj.Name}""");
            tw.WriteLine(")]");
            WriteOptions(ctx, obj.Options);
            tw = ctx.Write($"{GetAccess(GetAccess(obj))} partial class {Escape(name)}");
            if (obj.ExtensionRanges.Count != 0) tw.Write(" : global::ProtoBuf.IExtensible");
            tw.WriteLine();
            ctx.WriteLine("{").Indent();
            if (obj.Options?.MessageSetWireFormat == true)
            {
                ctx.WriteLine("#error message_set_wire_format is not currently implemented").WriteLine();
            }
            if (obj.ExtensionRanges.Count != 0)
            {
                ctx.WriteLine($"private global::ProtoBuf.IExtension {FieldPrefix}extensionData;")
                    .WriteLine($"global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)").Indent()
                    .WriteLine($"=> global::ProtoBuf.Extensible.GetExtensionObject(ref {FieldPrefix}extensionData, createIfMissing);").Outdent().WriteLine();
            }
        }

        private static void WriteOptions<T>(GeneratorContext ctx, T obj) where T : class, ISchemaOptions
        {
            if (obj == null) return;
            if (obj.Deprecated)
            {
                ctx.WriteLine($"[global::System.Obsolete]");
            }
        }

        const string FieldPrefix = "__pbn__";

        /// <summary>
        /// Get the language specific keyword representing an access level
        /// </summary>
        public override string GetAccess(Access access)
        {
            switch (access)
            {
                case Access.Internal: return "internal";
                case Access.Public: return "public";
                case Access.Private: return "private";
                default: return base.GetAccess(access);
            }
        }
        static HashSet<string> TypeNames2 = new HashSet<string>();
        static string GetTypeName2(string type) {
            if (type.StartsWith(".")) { type = type.Substring(1); }
            TypeNames2.Add(type);
            return type;
        }
		public static void ClearTypeNames(){
			TypeNames2.Clear ();
		}
        /// <summary>
        /// Write a field
        /// </summary>
        protected override void WriteField(GeneratorContext ctx, FieldDescriptorProto obj, ref object state, OneOfStub[] oneOfs)
        {
            var name = ctx.NameNormalizer.GetName(obj);
            var tw = ctx.Write($@"[global::ProtoBuf.ProtoMember({obj.Number}");
            if (name != obj.Name)
            {
                tw.Write($@", Name = @""{obj.Name}""");
            }
            var options = obj.Options?.GetOptions();
            if (options?.AsReference == true)
            {
                tw.Write($@", AsReference = true");
            }
            if (options?.DynamicType == true)
            {
                tw.Write($@", DynamicType = true");
            }

            bool isOptional = obj.label == FieldDescriptorProto.Label.LabelOptional;
            bool isRepeated = obj.label == FieldDescriptorProto.Label.LabelRepeated;
            if (isRepeated && obj.type == FieldDescriptorProto.Type.TypeMessage)
            {
                tw.Write($@", TypeName = ""{GetTypeName2(obj.TypeName)}""");
            }
            OneOfStub oneOf = obj.ShouldSerializeOneofIndex() ? oneOfs?[obj.OneofIndex] : null;
            if (oneOf != null && oneOf.CountTotal == 1)
            {
                oneOf = null; // not really a one-of, then!
            }
            bool explicitValues = isOptional && oneOf == null && ctx.Syntax == FileDescriptorProto.SyntaxProto2
                && obj.type != FieldDescriptorProto.Type.TypeMessage
                && obj.type != FieldDescriptorProto.Type.TypeGroup;


            string defaultValue = null;
            bool suppressDefaultAttribute = !isOptional;
            if (isOptional || obj.type == FieldDescriptorProto.Type.TypeEnum)
            {
				//GetTypeName2(obj.TypeName);
                defaultValue = obj.DefaultValue;

                if (obj.type == FieldDescriptorProto.Type.TypeString)
                {
                    defaultValue = string.IsNullOrEmpty(defaultValue) ? "\"\""
                        : ("@\"" + (defaultValue ?? "").Replace("\"", "\"\"") + "\"");
                }
                else if (obj.type == FieldDescriptorProto.Type.TypeDouble)
                {
                    switch (defaultValue)
                    {
                        case "inf": defaultValue = "double.PositiveInfinity"; break;
                        case "-inf": defaultValue = "double.NegativeInfinity"; break;
                        case "nan": defaultValue = "double.NaN"; break;
                    }
                }
                else if (obj.type == FieldDescriptorProto.Type.TypeFloat)
                {
                    switch (defaultValue)
                    {
                        case "inf": defaultValue = "float.PositiveInfinity"; break;
                        case "-inf": defaultValue = "float.NegativeInfinity"; break;
                        case "nan": defaultValue = "float.NaN"; break;
                    }
                }
                else if (obj.type == FieldDescriptorProto.Type.TypeEnum)
                {
                    var enumType = ctx.TryFind<EnumDescriptorProto>(obj.TypeName);
                    if (enumType != null)
                    {
                        EnumValueDescriptorProto found = null;
                        if (!string.IsNullOrEmpty(defaultValue))
                        {
                            found = enumType.Values.FirstOrDefault(x => x.Name == defaultValue);
                        }
                        else if (ctx.Syntax == FileDescriptorProto.SyntaxProto2)
                        {
                            // find the first one; if that is a zero, we don't need it after all
                            found = enumType.Values.FirstOrDefault();
                            if(found != null && found.Number == 0)
                            {
                                if(!isOptional) found = null; // we don't need it after all
                            }
                        }
                        // for proto3 the default is 0, so no need to do anything - GetValueOrDefault() will do it all

                        if (found != null)
                        {
                            defaultValue = ctx.NameNormalizer.GetName(found);
                        }
                        if (!string.IsNullOrWhiteSpace(defaultValue))
                        {
                            //defaultValue = ctx.NameNormalizer.GetName(enumType) + "." + defaultValue;

							defaultValue = "global::"+enumType.FullyQualifiedName.Substring(1) + "." + defaultValue;
                        }
                    }
                }
            }
            string dataFormat;
            bool isMap;
            var typeName = GetTypeName(ctx, obj, out dataFormat, out isMap);
            if (!string.IsNullOrWhiteSpace(dataFormat))
            {
                tw.Write($", DataFormat = global::ProtoBuf.DataFormat.{dataFormat}");
            }
            if (obj.IsPacked(ctx.Syntax))
            {
                tw.Write($", IsPacked = true");
            }
            if (obj.label == FieldDescriptorProto.Label.LabelRequired)
            {
                tw.Write($", IsRequired = true");
            }
            tw.WriteLine(")]");
            if (!isRepeated && !string.IsNullOrWhiteSpace(defaultValue) && !suppressDefaultAttribute)
            {
                ctx.WriteLine($"[global::System.ComponentModel.DefaultValue({defaultValue})]");
            }
            WriteOptions(ctx, obj.Options);
            if (isRepeated)
            {
                var mapMsgType = isMap ? ctx.TryFind<DescriptorProto>(obj.TypeName) : null;
                if (mapMsgType != null)
                {
                    string keyDataFormat;
                    bool _;
                    var keyTypeName = GetTypeName(ctx, mapMsgType.Fields.Single(x => x.Number == 1),
                        out keyDataFormat, out _);
                    string valueDataFormat;
                    var valueTypeName = GetTypeName(ctx, mapMsgType.Fields.Single(x => x.Number == 2),
                        out valueDataFormat, out _);

                    bool first = true;
                    tw = ctx.Write($"[global::ProtoBuf.ProtoMap");
                    if (!string.IsNullOrWhiteSpace(keyDataFormat))
                    {
                        tw.Write($"{(first ? "(" : ", ")}KeyFormat = global::ProtoBuf.DataFormat.{keyDataFormat}");
                        first = false;
                    }
                    if (!string.IsNullOrWhiteSpace(valueDataFormat))
                    {
                        tw.Write($"{(first ? "(" : ", ")}ValueFormat = global::ProtoBuf.DataFormat.{valueDataFormat}");
                        first = false;
                    }
                    tw.WriteLine(first ? "]" : ")]");
                    ctx.WriteLine($"{GetAccess(GetAccess(obj))} global::System.Collections.Generic.Dictionary<{keyTypeName}, {valueTypeName}> {Escape(name)} {{ get; }} = new global::System.Collections.Generic.Dictionary<{keyTypeName}, {valueTypeName}>();");
                }
                else if (UseArray(obj))
                {
                    ctx.WriteLine($"{GetAccess(GetAccess(obj))} {typeName}[] {Escape(name)} {{ get; set; }}");
                }
                else
                {
                    ctx.WriteLine($"{GetAccess(GetAccess(obj))} global::System.Collections.Generic.List<{typeName}> {Escape(name)} {{ get; }} = new global::System.Collections.Generic.List<{typeName}>();");
                }
            }
            else if (oneOf != null)
            {
                var defValue = string.IsNullOrWhiteSpace(defaultValue) ? $"default({typeName})" : defaultValue;
                var fieldName = FieldPrefix + oneOf.OneOf.Name;
                var storage = oneOf.GetStorage(obj.type, obj.TypeName);
                ctx.WriteLine($"{GetAccess(GetAccess(obj))} {typeName} {Escape(name)}").WriteLine("{").Indent();

                switch (obj.type)
                {
                    case FieldDescriptorProto.Type.TypeMessage:
                    case FieldDescriptorProto.Type.TypeGroup:
                    case FieldDescriptorProto.Type.TypeEnum:
                    case FieldDescriptorProto.Type.TypeBytes:
                    case FieldDescriptorProto.Type.TypeString:
                        ctx.WriteLine($"get {{ return {fieldName}.Is({obj.Number}) ? (({typeName}){fieldName}.{storage}) : {defValue}; }}");
                        break;
                    default:
                        ctx.WriteLine($"get {{ return {fieldName}.Is({obj.Number}) ? {fieldName}.{storage} : {defValue}; }}");
                        break;
                }
                var unionType = oneOf.GetUnionType();
                ctx.WriteLine($"set {{ {fieldName} = new global::ProtoBuf.{unionType}({obj.Number}, value); }}")
                    .Outdent().WriteLine("}")
                    .WriteLine($"{GetAccess(GetAccess(obj))} bool ShouldSerialize{name}() => {fieldName}.Is({obj.Number});")
                    .WriteLine($"{GetAccess(GetAccess(obj))} void Reset{name}() => global::ProtoBuf.{unionType}.Reset(ref {fieldName}, {obj.Number});");

                if (oneOf.IsFirst())
                {
                    ctx.WriteLine().WriteLine($"private global::ProtoBuf.{unionType} {fieldName};");
                }
            }
            else if (explicitValues)
            {
                string fieldName = FieldPrefix + name, fieldType;
                bool isRef = false;
                switch (obj.type)
                {
                    case FieldDescriptorProto.Type.TypeString:
                    case FieldDescriptorProto.Type.TypeBytes:
                        fieldType = typeName;
                        isRef = true;
                        break;
                    default:
                        fieldType = typeName + "?";
                        break;
                }
                ctx.WriteLine($"{GetAccess(GetAccess(obj))} {typeName} {Escape(name)}").WriteLine("{").Indent();
                tw = ctx.Write($"get {{ return {fieldName}");
                if (!string.IsNullOrWhiteSpace(defaultValue))
                {
                    tw.Write(" ?? ");
                    tw.Write(defaultValue);
                }
                else if (!isRef)
                {
                    tw.Write(".GetValueOrDefault()");
                }
                tw.WriteLine("; }");
                ctx.WriteLine($"set {{ {fieldName} = value; }}")
                    .Outdent().WriteLine("}")
                    .WriteLine($"{GetAccess(GetAccess(obj))} bool ShouldSerialize{name}() => {fieldName} != null;")
                    .WriteLine($"{GetAccess(GetAccess(obj))} void Reset{name}() => {fieldName} = null;")
                    .WriteLine($"private {fieldType} {fieldName};");
            }
            else
            {
                tw = ctx.Write($"{GetAccess(GetAccess(obj))} {typeName} {Escape(name)} {{ get; set; }}");
                if (!string.IsNullOrWhiteSpace(defaultValue)) tw.Write($" = {defaultValue};");
                tw.WriteLine();
            }
            ctx.WriteLine();
        }
        /// <summary>
        /// Starts an extgensions block
        /// </summary>
        protected override void WriteExtensionsHeader(GeneratorContext ctx, FileDescriptorProto obj, ref object state)
        {
            var name = obj?.Options?.GetOptions()?.ExtensionTypeName;
            if (string.IsNullOrWhiteSpace(name)) name = "Extensions";
            ctx.WriteLine($"{GetAccess(GetAccess(obj))} static class {Escape(name)}").WriteLine("{").Indent();
        }
        /// <summary>
        /// Ends an extgensions block
        /// </summary>
        protected override void WriteExtensionsFooter(GeneratorContext ctx, FileDescriptorProto obj, ref object state)
        {
            ctx.Outdent().WriteLine("}");
        }
        /// <summary>
        /// Starts an extensions block
        /// </summary>
        protected override void WriteExtensionsHeader(GeneratorContext ctx, DescriptorProto obj, ref object state)
        {
            var name = obj?.Options?.GetOptions()?.ExtensionTypeName;
            if (string.IsNullOrWhiteSpace(name)) name = "Extensions";
            ctx.WriteLine($"{GetAccess(GetAccess(obj))} static class {Escape(name)}").WriteLine("{").Indent();
        }
        /// <summary>
        /// Ends an extensions block
        /// </summary>
        protected override void WriteExtensionsFooter(GeneratorContext ctx, DescriptorProto obj, ref object state)
        {
            ctx.Outdent().WriteLine("}");
        }
        /// <summary>
        /// Write an extension
        /// </summary>
        protected override void WriteExtension(GeneratorContext ctx, FieldDescriptorProto field)
        {
            string dataFormat;
            bool isMap;
            var type = GetTypeName(ctx, field, out dataFormat, out isMap);

            if (isMap)
            {
                ctx.WriteLine("#error map extensions not yet implemented");
            }
            else if (field.label == FieldDescriptorProto.Label.LabelRepeated)
            {
                ctx.WriteLine("#error repeated extensions not yet implemented");
            }
            else
            {
                var msg = ctx.TryFind<DescriptorProto>(field.Extendee);
                var extendee = MakeRelativeName(field, msg, ctx.NameNormalizer);

                var @this = field.Parent is FileDescriptorProto ? "this " : "";
                string name = ctx.NameNormalizer.GetName(field);
                var tw = ctx.WriteLine($"{GetAccess(GetAccess(field))} static {type} Get{name}({@this}{extendee} obj)")
                    .Write($"=> obj == null ? default({type}) : global::ProtoBuf.Extensible.GetValue<{type}>(obj, {field.Number}");
                if (!string.IsNullOrEmpty(dataFormat))
                {
                    tw.Write($", global::ProtoBuf.DataFormat.{dataFormat}");
                }
                tw.WriteLine(");");
                ctx.WriteLine();
                //  GetValue<TValue>(IExtensible instance, int tag, DataFormat format)
            }
        }

        private static bool UseArray(FieldDescriptorProto field)
        {
            switch (field.type)
            {
                case FieldDescriptorProto.Type.TypeBool:
                case FieldDescriptorProto.Type.TypeDouble:
                case FieldDescriptorProto.Type.TypeFixed32:
                case FieldDescriptorProto.Type.TypeFixed64:
                case FieldDescriptorProto.Type.TypeFloat:
                case FieldDescriptorProto.Type.TypeInt32:
                case FieldDescriptorProto.Type.TypeInt64:
                case FieldDescriptorProto.Type.TypeSfixed32:
                case FieldDescriptorProto.Type.TypeSfixed64:
                case FieldDescriptorProto.Type.TypeSint32:
                case FieldDescriptorProto.Type.TypeSint64:
                case FieldDescriptorProto.Type.TypeUint32:
                case FieldDescriptorProto.Type.TypeUint64:
                    return true;
                default:
                    return false;
            }
        }

        private string GetTypeName(GeneratorContext ctx, FieldDescriptorProto field, out string dataFormat, out bool isMap)
        {
            dataFormat = "";
            isMap = false;
            switch (field.type)
            {
                case FieldDescriptorProto.Type.TypeDouble:
                    return "double";
                case FieldDescriptorProto.Type.TypeFloat:
                    return "float";
                case FieldDescriptorProto.Type.TypeBool:
                    return "bool";
                case FieldDescriptorProto.Type.TypeString:
                    return "string";
                case FieldDescriptorProto.Type.TypeSint32:
                    dataFormat = nameof(DataFormat.ZigZag);
                    return "int";
                case FieldDescriptorProto.Type.TypeInt32:
                    return "int";
                case FieldDescriptorProto.Type.TypeSfixed32:
                    dataFormat = nameof(DataFormat.FixedSize);
                    return "int";
                case FieldDescriptorProto.Type.TypeSint64:
                    dataFormat = nameof(DataFormat.ZigZag);
                    return "long";
                case FieldDescriptorProto.Type.TypeInt64:
                    return "long";
                case FieldDescriptorProto.Type.TypeSfixed64:
                    dataFormat = nameof(DataFormat.FixedSize);
                    return "long";
                case FieldDescriptorProto.Type.TypeFixed32:
                    dataFormat = nameof(DataFormat.FixedSize);
                    return "uint";
                case FieldDescriptorProto.Type.TypeUint32:
                    return "uint";
                case FieldDescriptorProto.Type.TypeFixed64:
                    dataFormat = nameof(DataFormat.FixedSize);
                    return "ulong";
                case FieldDescriptorProto.Type.TypeUint64:
                    return "ulong";
                case FieldDescriptorProto.Type.TypeBytes:
                    return "byte[]";
                case FieldDescriptorProto.Type.TypeEnum:
                    switch (field.TypeName)
                    {
                        case ".bcl.DateTime.DateTimeKind":
                            return "global::System.DateTimeKind";
                    }
                    var enumType = ctx.TryFind<EnumDescriptorProto>(field.TypeName);
                    return MakeRelativeName(field, enumType, ctx.NameNormalizer);
                case FieldDescriptorProto.Type.TypeGroup:
                case FieldDescriptorProto.Type.TypeMessage:
                    switch (field.TypeName)
                    {
                        case WellKnownTypeTimestamp:
                            dataFormat = "WellKnown";
                            return "global::System.DateTime?";
                        case WellKnownTypeDuration:
                            dataFormat = "WellKnown";
                            return "global::System.TimeSpan?";
                        case ".bcl.NetObjectProxy":
                            return "object";
                        case ".bcl.DateTime":
                            return "global::System.DateTime?";
                        case ".bcl.TimeSpan":
                            return "global::System.TimeSpan?";
                        case ".bcl.Decimal":
                            return "decimal?";
                        case ".bcl.Guid":
                            return "global::System.Guid?";
                    }
                    var msgType = ctx.TryFind<DescriptorProto>(field.TypeName);
                    if (field.type == FieldDescriptorProto.Type.TypeGroup)
                    {
                        dataFormat = nameof(DataFormat.Group);
                    }
                    isMap = msgType?.Options?.MapEntry ?? false;
                    return MakeRelativeName(field, msgType, ctx.NameNormalizer);
                default:
                    return field.TypeName;
            }
        }

        private string MakeRelativeName(FieldDescriptorProto field, IType target, NameNormalizer normalizer)
        {
            if (target == null) return Escape(field.TypeName); // the only thing we know

            var declaringType = field.Parent;

            if (declaringType is IType)
            {
                var name = FindNameFromCommonAncestor((IType)declaringType, target, normalizer);
                if (!string.IsNullOrWhiteSpace(name)) return name;
            }
            return Escape(field.TypeName); // give up!
        }

        // k, what we do is; we have two types; each knows the parent, but nothing else, so:
        // for each, use a stack to build the ancestry tree - the "top" of the stack will be the
        // package, the bottom of the stack will be the type itself. They will often be stacks
        // of different heights.
        //
        // Find how many is in the smallest stack; now take that many items, in turn, until we
        // get something that is different (at which point, put that one back on the stack), or 
        // we run out of items in one of the stacks.
        //
        // There are now two options:
        // - we ran out of things in the "target" stack - in which case, they are common enough to not
        //   need any resolution - just give back the fixed name
        // - we have things left in the "target" stack - in which case we have found a common ancestor,
        //   or the target is a descendent; either way, just concat what is left (including the package
        //   if the package itself was different)

        private string FindNameFromCommonAncestor(IType declaring, IType target, NameNormalizer normalizer)
        {
            // trivial case; asking for self, or asking for immediate child
            if (ReferenceEquals(declaring, target) || ReferenceEquals(declaring, target.Parent))
            {
                if (target is DescriptorProto) return Escape(normalizer.GetName((DescriptorProto)target));
                if (target is EnumDescriptorProto) return Escape(normalizer.GetName((EnumDescriptorProto)target));
                return null;
            }

            var origTarget = target;
            var xStack = new Stack<IType>();

            while (declaring != null)
            {
                xStack.Push(declaring);
                declaring = declaring.Parent;
            }
            var yStack = new Stack<IType>();

            while (target != null)
            {
                yStack.Push(target);
                target = target.Parent;
            }
            int lim = Math.Min(xStack.Count, yStack.Count);
            for (int i = 0; i < lim; i++)
            {
                declaring = xStack.Peek();
                target = yStack.Pop();
                if (!ReferenceEquals(target, declaring))
                {
                    // special-case: if both are the package (file), and they have the same namespace: we're OK
                    if (target is FileDescriptorProto && declaring is FileDescriptorProto &&
                        normalizer.GetName((FileDescriptorProto)declaring) == normalizer.GetName((FileDescriptorProto)target))
                    {
                        // that's fine, keep going
                    }
                    else
                    {
                        // put it back
                        yStack.Push(target);
                        break;
                    }
                }
            }
            // if we used everything, then the target is an ancestor-or-self
            if (yStack.Count == 0)
            {
                target = origTarget;
                if (target is DescriptorProto) return Escape(normalizer.GetName((DescriptorProto)target));
                if (target is EnumDescriptorProto) return Escape(normalizer.GetName((EnumDescriptorProto)target));
                return null;
            }

            var sb = new StringBuilder();
            while (yStack.Count != 0)
            {
                target = yStack.Pop();

                string nextName;
                if (target is FileDescriptorProto) nextName = normalizer.GetName((FileDescriptorProto)target);
                else if (target is DescriptorProto) nextName = normalizer.GetName((DescriptorProto)target);
                else if (target is EnumDescriptorProto) nextName = normalizer.GetName((EnumDescriptorProto)target);
                else return null;

                if (!string.IsNullOrWhiteSpace(nextName))
                {
                    if (sb.Length == 0 && target is FileDescriptorProto) sb.Append("global::");
                    else if (sb.Length != 0) sb.Append('.');
                    sb.Append(Escape(nextName));
                }
            }
            return sb.ToString();
        }

        static bool IsAncestorOrSelf(IType parent, IType child)
        {
            while (parent != null)
            {
                if (ReferenceEquals(parent, child)) return true;
                parent = parent.Parent;
            }
            return false;
        }
        const string WellKnownTypeTimestamp = ".google.protobuf.Timestamp",
                     WellKnownTypeDuration = ".google.protobuf.Duration";
    }
}
